package org.tangolyricstagger;

import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.ProgressBar;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileFilter;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import org.jsoup.Jsoup;
import org.junit.Test;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.tangolyricstagger.gui.GenreModelProvider;
import org.tangolyricstagger.gui.MainWindow;
import org.tangolyricstagger.gui.MatchResultModelProvider;
import org.tangolyricstagger.gui.Settings;
import org.tangolyricstagger.model.MatchResult;
import org.tangolyricstagger.model.TagInfo;
import org.tangolyricstagger.model.TagScanResults;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

@Slf4j
public class MatchTitlesTest {
	TagReader tagReader = new TagReader();

	private Map<String, MatchResult> matchesCache = new HashMap<String, MatchResult>();
	private Map<MatchResult, LyricsInfo> lyricsCache = new HashMap<MatchResult, LyricsInfo>();
	boolean stopRequested = false;

	private ProgressBar progressBar;
	private Label progressLabel;

	private Map<String, MatchResult> matchStatistics = new HashMap<String, MatchResult>();

	private void setProgress(final String whatsHappening, final Long processedNumber, final Long totalNumber, final int percOffset, final int percMax) {
		progressBar.getDisplay().syncExec(new Runnable() {
			public void run() {
				int selection = 0;
				String text = "";
				if (totalNumber != null) {
					double completion = ((double) processedNumber) / totalNumber;
					selection = (int) (percOffset + Math.round(completion * percMax));
					text = "overall progress: " + selection + "%";
					if (processedNumber != null) {
						text = " (" + processedNumber + "/" + totalNumber + ") - " + text;
					}
					progressLabel.setText(text);
				} else {
					selection = percOffset;
					text = "" + processedNumber;
				}
				progressBar.setSelection(selection);
				progressLabel.setText(whatsHappening + ": " + text);
			}
		});

	}

	@Test
	public void testMatchTitles(MainWindow mainWindow, Settings settings, ProgressBar progressBar, Label progressLabel,
	        GenreModelProvider genreModel, MatchResultModelProvider matchResultModel) {
		try {
			this.progressBar = progressBar;
			this.progressLabel = progressLabel;
			SLF4JBridgeHandler.removeHandlersForRootLogger();
			SLF4JBridgeHandler.install();
			String[] titles = parseSongs();
			TagScanResults tagScanResults = find(settings);
			Set<String> ignoredGenres = tagScanResults.getIgnoredGenres();
			for (String genre : ignoredGenres) {
				genreModel.notifyIgnored(genre);
			}
			Map<String, Long> genreStatistics = tagScanResults.getGenreStatistics();
			for (String genre : genreStatistics.keySet()) {
				genreModel.add(genre, genreStatistics.get(genre));
			}
			mainWindow.refreshGenres();

			List<TagInfo> tags = tagScanResults.getTagInfos();

			MatchTitle matchTitle = new MatchTitle();

			long numberOfFiles = tags.size();
			log.info("Processing " + numberOfFiles + " files");
			long i = 0;
			for (TagInfo tagInfo : tags) {
				if (stopRequested) {
					log.info("Stop requested");
					break;
				}
				i++;

				String originalTitle = tagInfo.getCompleteTitle();
				log.info("--------------------------------------------------------------------------------------------\n'" + originalTitle + "'");

				tagInfo.cleanTitle(settings.getTakeTitleBeforeSeparators());
				String candidateTitle = tagInfo.getCompleteTitle();
				MatchResult matchResult = matchesCache.get(candidateTitle);
				if (matchResult == null) {
					matchResult = matchTitle.findMatch(originalTitle, titles, tagInfo);
					matchesCache.put(candidateTitle, matchResult);
				} else {
					if (matchResult.isMatched()) {
						log.info("Found match in cache");
					}
				}

				double completion = ((double) i) / numberOfFiles;
				System.out.println(completion);
				setProgress("Matching songs", i, numberOfFiles, 10, 90);

				registerMatch(originalTitle, matchResult, matchResultModel);
				if ((i) % 10 == 0) {
					mainWindow.refreshMatchResults();
				}

				if (matchResult.isMatched()) {
					String matchedTitle = matchResult.getMatchedTitle();
					log.info("Trimmed title: '" + candidateTitle + "', TodoTango match found: '" + matchedTitle + "' (Levenshtein "
					        + matchResult.getSimilarity() + ")");

					LyricsInfo lyricsInfo = lyricsCache.get(matchResult);
					if (lyricsInfo == null) {
						if (matchedTitle != null) {
							String urlString = "http://www.todotango.com/spanish/Las_obras/Tema.aspx?t="
							        + StringUtils.replace(matchedTitle, " ", "%20");
							org.jsoup.nodes.Document doc = Jsoup.parse(new URL(urlString), 10000);
							String titulo = getLblValue(doc, "Titulo");
							String autorLetra = getHlValue(doc, "AutorLetra1");
							String autorMusica = getHlValue(doc, "AutorMusica1");
							String letra = getLblValue(doc, "Letra");
							if (!StringUtils.isBlank(letra)) {
								autorLetra = StringEscapeUtils.unescapeHtml(autorLetra);
								autorMusica = StringEscapeUtils.unescapeHtml(autorMusica);
								titulo = StringEscapeUtils.unescapeHtml(titulo);
								letra = StringEscapeUtils.unescapeHtml(letra);
								letra = StringUtils.replace(letra, "<br />", "\n");
								letra += "\n\n(Todotango title: '" + titulo + "')";
								log.info("Found lyrics: \n" + letra);
								lyricsInfo = new LyricsInfo(letra, titulo, autorMusica, autorLetra);
								lyricsCache.put(matchResult, lyricsInfo);
								matchResult.setTodoTangoTitle(titulo);
								matchResult.setLyrics(letra);
							} else {
								log.info("No lyrics found");
							}
						}
					} else {
						log.info("Found lyrics in cache");
					}
					if (lyricsInfo != null) {
						try {
							AudioFile audioFile = AudioFileIO.read(tagInfo.getFile());
							String lyrics = audioFile.getTag().getFirst(FieldKey.LYRICS);
							if (isBlank(lyrics) || settings.isOverwriteLyrics()) {
								log.info("Writing tags to file " + audioFile.getFile().getAbsolutePath());
								lyrics = lyricsInfo.getLetra();
								String year = tagInfo.getYear();
								if (isNotBlank(year) && settings.isAppendRecordingYear()) {
									lyrics = "Recording year: " + year + "\n\n" + lyrics;
								}
								audioFile.getTag().deleteField(FieldKey.LYRICS);
								audioFile.getTag().setField(FieldKey.LYRICS, lyrics);
								if (settings.isFillComposerTag()) {
									String composer = createComposer(lyricsInfo);
									if (StringUtils.isNotBlank(composer)) {
										audioFile.getTag().setField(FieldKey.COMPOSER, composer);
									}
								}
								audioFile.commit();
							}
						} catch (Exception e) {
							System.err.println(e.getMessage());
						}
					}
				} else {
					log.info("No match found");
				}
			}
			mainWindow.refreshMatchResults();

		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
	}

	private void registerMatch(String originalTitle, MatchResult matchResult, MatchResultModelProvider matchResultModel) {
		if (!matchStatistics.containsKey(originalTitle)) {
			matchStatistics.put(originalTitle, matchResult);
			matchResultModel.add(matchResult);
		}
	}

	private String createComposer(LyricsInfo lyricsInfo) {
		StringBuffer sb = new StringBuffer();
		String autorMusica = lyricsInfo.getAutorMusica();
		String autorLetra = lyricsInfo.getAutorLetra();
		if (StringUtils.isNotBlank(autorMusica)) {
			sb.append("Music: " + autorMusica);
		}
		if (StringUtils.isNotBlank(autorLetra)) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append("Lyrics: " + autorLetra);
		}
		return sb.toString();
	}

	private String getHlValue(org.jsoup.nodes.Document doc, String idPostfix) {
		String result = getValue(doc, "hl", idPostfix);
		if (StringUtils.isBlank(result)) {
			// try lbl
			result = getValue(doc, "lbl", idPostfix);
		}
		return result;
	}

	private String getLblValue(org.jsoup.nodes.Document doc, String idPostfix) {
		String result = getValue(doc, "lbl", idPostfix);
		if (StringUtils.isBlank(result)) {
			// try hl
			result = getValue(doc, "hl", idPostfix);
		}
		return result;
	}

	private String getValue(org.jsoup.nodes.Document doc, String middleId, String idPostfix) {
		String tryId1 = "#ctl00_Main_" + middleId + "_" + idPostfix;
		String result = doc.select(tryId1).html();
		if (StringUtils.isBlank(result)) {
			String tryId2 = "#ctl00_ContentPlaceHolder1_" + middleId + "_" + idPostfix;
			log.info("No result on todotango for " + tryId1 + ", trying " + tryId2);
			result = doc.select(tryId2).html();
			if (StringUtils.isBlank(result)) {
				log.info("Still no success, giving up for '" + middleId + "'");
			}
		}
		return result;
	}

	public String[] parseSongs() throws Exception {
		InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("temas.xml");
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = dbf.newDocumentBuilder();

		Document doc = db.parse(stream);
		doc.getDocumentElement().normalize();
		System.out.println("Root element " + doc.getDocumentElement().getNodeName());

		List<String> results = new ArrayList<String>();
		NodeList nodeLst = doc.getElementsByTagName("Titulo");
		for (int s = 0; s < nodeLst.getLength(); s++) {
			Node node = nodeLst.item(s);
			String title = StringUtils.lowerCase(node.getFirstChild().getNodeValue());
			results.add(title);
		}
		String[] resultArray = new String[results.size()];
		results.toArray(resultArray);
		return resultArray;
	}

	public void requestStop() {
		stopRequested = true;
	}

	public TagScanResults find(Settings settings) throws FileNotFoundException {
		TagScanResults results = new TagScanResults();
		File rootDirectory = new File(settings.getDirectory());
		String genresString = settings.getGenres();
		String[] genres = StringUtils.split(genresString, ',');
		for (int i = 0; i < genres.length; i++) {
			genres[i] = StringUtils.strip(genres[i]);
		}
		validateDirectory(rootDirectory);
		List<File> files = new ArrayList<File>();
		getFileListing(files, rootDirectory);

		// print out all file names, in the the order of File.compareTo()

		log.info("Start reading tags in files, rootDirectory=" + rootDirectory.getAbsolutePath());
		long i = 0;
		long numberOfFiles = files.size();
		for (File file : files) {
			if (stopRequested) {
				log.info("Stop requested");
				break;
			}

			TagInfo tagInfo;
			try {
				tagInfo = tagReader.createTagInfo(file);
			} catch (Exception e) {
				System.err.println(e.getMessage());
				continue;
			}
			boolean considerGenre = false;
			if (genres.length == 0) {
				considerGenre = true;
			} else {
				String genre = tagInfo.getGenre();
				for (String consideredGenre : genres) {
					if (StringUtils.containsIgnoreCase(genre, consideredGenre)) {
						considerGenre = true;
						break;
					}
				}
				if (!considerGenre && !results.containsIgnoredGenre(genre)) {
					results.addIgnoredGenre(genre);
					log.info("Ignored genre: " + genre + " because it does not contain one of " + ArrayUtils.toString(genres)
					        + ". This message will be displayed only once per ignored genre.");
				}
			}
			if (considerGenre) {
				results.addTagInfo(tagInfo);
			}
			if ((++i % 500) == 0) {
				log.info(i + " files analyzed");
				setProgress("Reading tags", i, numberOfFiles, 0, 10);
			}
		}

		results.sortTagInfos();
		return results;
	}

	private void getFileListing(List<File> result, File aStartingDir) throws FileNotFoundException {
		File[] filesAndDirs = aStartingDir.listFiles(new AudioFileFilter(true));
		List<File> filesDirs = Arrays.asList(filesAndDirs);
		for (File file : filesDirs) {
			if (!file.isFile()) {
				// must be a directory recursive call!
				setProgress("Scanning for songs", (long) result.size(), null, 0, 2);
				getFileListing(result, file);
			} else {
				result.add(file);
			}
		}
	}

	/**
	 * Directory is valid if it exists, does not represent a file, and can be read.
	 */
	static private void validateDirectory(File aDirectory) throws FileNotFoundException {
		if (aDirectory == null) {
			throw new IllegalArgumentException("Directory should not be null.");
		}
		if (!aDirectory.exists()) {
			throw new FileNotFoundException("Directory does not exist: " + aDirectory);
		}
		if (!aDirectory.isDirectory()) {
			throw new IllegalArgumentException("Is not a directory: " + aDirectory);
		}
		if (!aDirectory.canRead()) {
			throw new IllegalArgumentException("Directory cannot be read: " + aDirectory);
		}
	}

}

@Getter
@AllArgsConstructor
class LyricsInfo {
	private String letra;
	private String titulo;
	private String autorMusica;
	private String autorLetra;
}
